Web GUI开发 - React
在很久以前,无意中接触到了 React,一直很喜欢它。工作中应该是用不到它了,但是一直想怎么去了解它。Web 前端开发中各种脚手架方便了入门使用,也屏蔽了太多东西。
多亏了现在 GPT 的强大以及 Google 良好的搜索能力,让我得以记录如何从0搭建一个 React Demo。
搭建项目
首先,需要安装 Node.js。
然后,创建项目文件夹(这里我们以 ReactDemo 为例)。进入项目文件夹,然后执行 npm init,它会引导你输入一系列信息创建项目,并最终生成 package.json 文件:
{
"name": "reactdemo",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}
安装 webpack 相关依赖:
npm install webpack webpack-cli webpack-dev-server
- webpack:构建工具
- webpack-cli:命令行运行 webpack 的工具
- webpack-dev-server:开发服务器
安装 webpack 依赖完成之后,package.json 会新增 dependencies 字段,记录依赖库及对应版本号:
{
// ......
"dependencies": {
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
}
// ......
}
安装 Babel 相关依赖:
npm install babel-loader @babel/preset-env @babel/core @babel/plugin-transform-runtime \
@babel/preset-react @babel/runtime @babel/cli
- babel-loader:webpack 的加载器(loader),用于将 JavaScript 代码通过 Babel 进行转换。
- @babel/preset-env:让我们可以使用最新的 JavaScript,而不用当心老版本的浏览器不兼容新ES标准。
- @babel/core:Babel 的核心模块,它是 babel 编译器的主要部分。
- @babel/plugin-transform-runtime:支持重复使用 Babel 注入的辅助代码以节省代码大小。
- @babel/preset-react:Babel 的一个预设(preset),用于转换 React 中的 JSX 语法。
- @babel/runtime:包含 polyfill 和许多其他 Babel 可以引用的包。
- @babel/cli:支持通过命令行运行 babel
安装 react 和 react-dom:
npm install react react-dom
在项目根目录下创建 public 文件夹,然后在该文件夹下创建 index.html 文件,在该文件添加如下内容:
<body>
<div id="root"></div>
</body>
<script src="/main.js"></script>
在项目根目录下创建 src 文件夹,然后在该文件夹下创建 App.jsx 文件,在该文件添加如下内容:
import React from "react";
export default function App() {
return (
<h1>
从 Webpack 和 Babel 开始搭建 React 项目
</h1>
)
}
在项目根目录下(或其他位置)创建 index.jsx,它将作为 webpack 的入口点(Entry Point)。在该文件添加如下内容:
import React from "react";
import { createRoot } from 'react-dom/client';
import App from "./src/App.jsx"
const root = createRoot(document.getElementById('root'));
root.render(<App />);
在项目根目录下创建一个名为 webpack.config.js 的文件并添加以下代码。此文件包含负责将代码文件打包为单个文件,以及设置开发服务器的配置。
const path = require("path");
module.exports = {
mode: "development", // or "production",
entry: "./index.jsx",
output: {
path: path.resolve(__dirname, "public"),
filename: "main.js"
},
target: "web",
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
host: "127.0.0.1",
port: "9000",
open: true,
hot: true,
liveReload: true
},
resolve: {
extensions: ['.js', '.jsx', '.json']
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react"]
}
}
]
}
}
更新 package.json 文件:
{
// ...
"scripts": {
"start": "webpack-dev-server .",
"build": "webpack .",
"test": "echo \"Error: no test specified\" && exit 1"
},
// ...
}
安装 React Router:
npm install react-router
使用 antd:
npm install antd
或使用安装 MUI:
npm install @mui/material @emotion/react @emotion/styled
减小打包体积
通过代码分割(Code Splitting)减小文件体积
React 项目通常会构建成一个庞大的 JavaScript 包,特别是当项目规模较大时。代码分割可以将你的应用拆分成更小的、按需加载的 chunk。
使用 React.lazy()、<Suspense/> 进行组件级代码分割:
import React, { lazy, Suspense } from "react";
const Demo = lazy(() => import("./Demo"))
export default function App() {
return (
<div>
<h1>
从 Webpack 和 Babel 开始搭建 React 项目
</h1>
<Suspense fallback="加载中...">
<Demo />
</Suspense>
</div>
)
}
上述方法,在页面路由下使用也非常有效,避免代码一次性加载的问题。
状态管理
将 useContext 与 useReducer 结合使用是一种强大的状态管理模式,类似于 React Redux,但更简单。
更新包
# 1. 检查可更新的包
npm outdated
# 2. 安全更新(遵循 package.json 中的版本范围)
npm update
在日常开发过程中,最常使用的元素布局方式可能就是 Flexbox 了,不过在编码实现过程中,经常可能遇到父元素莫名奇妙被撑高,或者子元素的大小不是我们预所期的。我想主要原因可能就是CSS的元素布局是从经典的文档流模型一步一步发展过来的,其中有很多默认属性影响着新的布局方式。一不小心漏掉什么就会使我们抓瞎半天。
CSS很强大,常见的布局它肯定都能实现,如果我们的实现无法按照我们所预期的表现,应该沉下心来了解一下基础知识。
HTML 几个基本概念
视口(Viewport):浏览器窗口中实际显示网页内容的区域,不包括地址栏、书签栏等浏览器界面。可以通过 100vh、100vw 单位引用其宽高。
文档流与盒模型:
HTML元素默认高度行为为 auto,及其高度由其内容决定:
/* 默认值 */
html {
height: auto; /* 由内容决定 */
}
body {
height: auto; /* 由内容决定 */
margin: 8px; /* 浏览器默认边距 */
}
在使用 Flexbox 布局时,我们一般希望父容器有明确的高度,所以一般推荐:
html, body {
height: 100%;
margin: 0;
}
CSS 元素布局可以分为一下几大类:正常流布局(Normal Flow)、浮动布局(Float)、定位布局(Positioning)、弹性盒子布局(Flexbox)、网格布局(Grid)。
其中正常流布局也即文档流布局,即像平常撰写的文档一样,块级元素垂直排列,独占一行;行内元素水平排列,直到占满容器宽度后换行。
CSS 测试模板代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html,
body,
#root {
height: 100%;
margin: 0;
}
#root {
display: flex;
flex-direction: column;
}
.item-a {
background-color: #88cce1;
}
.item-b {
flex-grow: 1;
background-color: #c68a9f;
display: flex;
flex-direction: row;
height: 0;
}
.item-c {
background-color: #80f29d;
}
.item-b-1 {
flex-grow: 1;
background-color: #7050e3;
}
.item-b-2 {
background-color: #b34040;
flex-grow: 10;
overflow: auto;
}
.content {
background-color: #cfd762;
height: 2000px;
}
</style>
</head>
<body>
<div id="root">
<div class="item-a">
Item A
</div>
<div class="item-b">
<div class="item-b-1">
Item B1
</div>
<div class="item-b-2">
<div class="content">
Content
</div>
</div>
</div>
<div class="item-c">
Item C
</div>
</div>
</body>
</html>
Flex Item 的默认最小尺寸
上述代码有一个问题,为什么 .item-b 必须设置 min-height: 0;,.item-b-2 的 overflow 属性才会生效。
这个问题涉及到 Flexbox 布局中的一个复杂但重要的概念:Flex Item 的默认最小尺寸 (Automatic Minimum Size)。
当一个 Flex 容器 (display: flex;) 设置了主轴方向(在这里是垂直方向,因为 #root 是 flex-direction: column;,而 .item-b 是 #root 的 Flex Item)时,其子元素(Flex Items)有一个默认的最小尺寸限制。
- 默认值不是 0: 对于 Flex Item 来说,其主轴方向(在这里是高度)的默认最小尺寸属性是
min-height: auto,而auto在 Flexbox 布局中通常解析为一个基于内容的最小尺寸(min-content),而不是 0。 - 内容决定尺寸: 这意味着 Flex Item 默认不会收缩到小于其内容的最小尺寸,即使你给它设置了
flex-shrink: 1;或使用了flex-grow试图让它占据剩余空间。它的最小高度会被它的内容(包括后代元素)撑开,以确保内容不会发生不必要的溢出。
在样板的代码中:
#root是一个垂直方向的 Flex 容器 (flex-direction: column;)。.item-b是#root中的一个 Flex Item,它被设置了flex-grow: 1;,目的是占据 A 和 C 之间所有剩余的垂直空间。.item-b-2是.item-b的子元素(也是一个 Flex Item,因为.item-b是display: flex;),它包含一个高度为2000px的.content元素。item-b-2设置了overflow: auto;,目的是让它内部出现滚动条。
当 .item-b 没有设置 min-height: 0; 时:
.item-b的默认最小高度 (min-height: auto) 被计算为其内容所需要的最小高度。.item-b的一个子元素是.item-b-2,而.item-b-2内部有2000px高度的内容。- 因此,
.item-b的最小高度被其内容(也就是.content的2000px高度)撑开,导致.item-b即使在flex-grow的计算中获得了剩余空间,其实际最小高度仍然是2000px左右。 - 这样,
.item-b在垂直方向上会变得非常高,超出了视口,导致最外层的body/html出现滚动条,而.item-b-2自身的overflow: auto;则不会触发,因为它并没有被约束在一个更小的尺寸内。
给 .item-b 添加 min-height: 0; 会覆盖 Flex Item 默认的 min-height: auto 行为。
- 它将
.item-b的最小尺寸设置回 0。 - 这样,当
.item-b在#root中计算高度时,它将能够自由收缩(由于flex-grow: 1;的作用,它会占据剩余空间,但同时它有一个 0 的最小限制),使其高度被限制在 A 和 C 之间的可用空间内。 - 一旦
.item-b的高度被限制,它的子元素.item-b-2也就被限制在一个明确的(且小于内容需求的)高度内。 - 此时,
.item-b-2内部的2000px内容就会超出.item-b-2的可用高度,从而触发.item-b-2上设置的overflow: auto;,产生内部滚动条,而不是让整个页面溢出。
简而言之,min-height: 0; 解除了 Flex Item 的内容对主轴尺寸的最小限制,允许它收缩到足以让内部可滚动区域生效的尺寸。
HTML元素的高度计算
HTML 元素的高度计算并不是一个单一的流程,而是取决于其设置的 height 属性值 和其所处的 布局环境。
总的来说,HTML 元素的高度计算是**自下而上(子元素到父元素)和自上而下(父元素到子元素)**两种机制的结合,这取决于为元素指定的是 内容高度 还是 百分比/视口高度。
自下而上 (子元素 → 父元素) - 默认/内容流
这是最常见、最基本的计算方式,也被称为收缩到合适(shrink-to-fit)或固有尺寸(Intrinsic Sizing)。机制:
当一个块级元素(如 <div> 或 <p>)的 height 属性设置为默认值 auto 时,其高度由其内容决定:
- 子元素内容撑开父元素:浏览器会先计算所有子元素(文本、图片、嵌套的盒子)的高度,然后父元素的高度会收缩或扩展到刚好能包含所有子元素的高度。
- 示例: 如果你有一个
<div>,里面放了三行文字,<div>的高度就会是这三行文字所需高度的总和。
自上而下 (父元素 → 子元素) - 百分比/弹性布局
当元素的高度依赖于其父元素的尺寸时,就会发生自上而下的计算。机制:
- 百分比高度 (
height: 50%):- 如果子元素的高度设置为百分比(例如
height: 50%),它必须先知道父元素确切的高度值,才能计算出自己的高度。 - 关键限制: 如果父元素的高度本身也是
auto(即由内容决定),那么子元素的百分比高度就会失效(被视为auto),除非父元素是 Flex 或 Grid 容器中的 Flex Item/Grid Item。 - 因此,要让百分比高度生效,需要有一条从
html或body开始(通常设置为height: 100%或height: 100vh)一直向下传递的明确高度链。
- 如果子元素的高度设置为百分比(例如
- 弹性布局 (Flexbox/Grid):
- 在 Flex 或 Grid 容器中,子元素(Flex Items/Grid Items)的尺寸计算会更加复杂,它可能依赖于容器的主轴和交叉轴尺寸、
flex-grow、flex-shrink等属性。 - 例如,如果父元素是垂直方向的 Flex 容器,子元素设置
flex-grow: 1;,那么父元素的高度需要先确定,然后子元素才能根据剩余空间来计算自己的高度。
- 在 Flex 或 Grid 容器中,子元素(Flex Items/Grid Items)的尺寸计算会更加复杂,它可能依赖于容器的主轴和交叉轴尺寸、
结论:一个循环的计算模型
现代浏览器在渲染时,会通过一个复杂的渲染树和布局算法来确定所有元素的最终尺寸,这个过程实际上是这两者的结合:
- 浏览器首先尝试通过内容来确定元素的固有尺寸(自下而上)。
- 然后,它检查是否有百分比或 Flexbox/Grid 规则,这要求它知道包含块的尺寸(自上而下)。
- 如果两者发生冲突或相互依赖,浏览器会遵循 CSS 规范中的复杂规则来解析,有时需要设置如
min-height: 0这样的属性来解除自下而上的最小尺寸限制,从而让自上而下的空间分配规则能够生效。